﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace Chisel.Core
{
	/// <summary>
	/// Handler for the actor events.
	/// </summary>
	public delegate void ActorEventHandler(Actor actor);

	/// <summary>
	/// Delegate for methods that validate that new text for a value is okay.
	/// </summary>
	/// <returns>True if the change is okay.</returns>
	public delegate bool TextChangedValidator(Actor actor, string text);

    /// <summary>
    /// Interface for things that can be drawn on a canvas.
    /// </summary>
    public abstract class Actor : UserControl
    {

		public Actor()
			: base()
		{
			State = new ActorState();
			Loaded += new RoutedEventHandler(OnLoaded);
			SizeChanged += new SizeChangedEventHandler(OnSizeChanged);

			_doubleClickTimer = new DispatcherTimer() {
				Interval = TimeSpan.FromMilliseconds(200)
			};
			_doubleClickTimer.Tick += new EventHandler(OnDoubleClickTick);
		}


		#region Loading

		/// <summary>
		/// Whether or not the actor has been loaded.
		/// </summary>
		public bool IsLoaded { get; private set; }

		protected virtual void OnLoaded(object sender, RoutedEventArgs e)
		{
			ResetStyle();
			IsLoaded = true;
		}

		#endregion


		#region General

		/// <summary>
		/// The current interaction state of the actor.
		/// </summary>
		public ActorState State { get; protected set; }

		/// <summary>
		/// The canvas this actor belongs to.
		/// </summary>
		public virtual DiagramCanvas ParentCanvas
		{
			get { return Parent as DiagramCanvas; }
		}

		/// <summary>
		/// The geometric center of the actor.
		/// </summary>
		public virtual Point Center
		{
			get
			{
				if (ParentCanvas != null)
					return new Point(Canvas.GetLeft(this) + ActualWidth / 2, Canvas.GetTop(this) + ActualHeight / 2);
				else
					return new Point();
			}
		}

		/// <summary>
		/// The Position of the actor in the canvas.
		/// </summary>
		public Point Position
		{
			get
			{
				if (ParentCanvas == null)
					return new Point();
				if (ParentCanvas.Children.Contains(this))
					return new Point(Canvas.GetLeft(this), Canvas.GetTop(this));
				else // not directly inside the canvas
				{
					var transform = ParentCanvas.TransformToVisual(this);
					var matrix = (transform as MatrixTransform).Matrix;
					return new Point(-matrix.OffsetX, -matrix.OffsetY);
				}
			}
			set
			{
				Canvas.SetLeft(this, value.X);
				Canvas.SetTop(this, value.Y);
			}
		}

		/// <summary>
		/// Resets the style based on the current hit state and the canvas style provider.
		/// </summary>
		protected virtual void ResetStyle()
		{
		}

		/// <summary>
		/// Convenience method that wraps ParentCanvas.StyleProvider.GetBrush();
		/// </summary>
		protected Brush GetBrush(BrushType brushType, ActorState state)
		{
			return ParentCanvas.StyleProvider.GetBrush(this, brushType, state);
		}

		/// <summary>
		/// Font size (from the style provider).
		/// </summary>
		protected double StyleFontSize
		{
			get { return ParentCanvas.StyleProvider.FontSize; }
		}

		#endregion


		#region Sizing
		
		/// <summary>
		/// This gets raised whenever the size changes.
		/// </summary>
		public ActorEventHandler Resized;

		/// <summary>
		/// Handles the size changed event.
		/// </summary>
		protected virtual void OnSizeChanged(object sender, SizeChangedEventArgs e)
		{
			if (Resized != null)
				Resized(this);
		}

		#endregion


		#region Selection

		/// <summary>
		/// This event gets raised whenever the selection state of the actor changes.
		/// </summary>
		public event ActorEventHandler SelectionChanged;

		/// <summary>
        /// This gets called by the actor collection when the actor gets selected.
        /// </summary>
        /// <remarks>Do not call this method to select the entity. Use DiagramCanvas.Actors.Select(actor) instead.</remarks>
		public virtual void OnSelect()
		{
			State.IsSelected = true;
			ResetStyle();

			if (SelectionChanged != null)
				SelectionChanged(this);
		}

		/// <summary>
		/// This gets called by the actor collection when the actor gets deselected.
		/// </summary>
		/// <remarks>Do not call this method to desselect the entity. Use DiagramCanvas.Actors.Deselect(entity) instead.</remarks>
		public virtual void OnDeselect()
		{
			State.IsSelected = false;
			State.IsDragging = false;
			ResetStyle();

			if (SelectionChanged != null)
				SelectionChanged(this);
		}

		/// <summary>
		/// Determine what happens in order to select this actor.
		/// </summary>
		/// <remarks>Default is ParentCanvas.Actors.Select(this), but this may not be appropriate for all subclasses.</remarks>
		protected virtual void SelectAction()
		{
			ParentCanvas.Actors.Select(this);
		}

		/// <summary>
		/// Determine what happens in order to deselect all actors related to this one.
		/// </summary>
		/// <remarks>Default is ParentCanvas.Actors.DeselectAll(this), but this may not be appropriate for all subclasses.</remarks>
		protected virtual void DeselectAllAction()
		{
			ParentCanvas.Actors.DeselectAll();
		}

		#endregion


		#region Mouse Interaction

		protected override void OnMouseEnter(MouseEventArgs e)
		{
			base.OnMouseEnter(e);

			State.IsHovering = true;
			ResetStyle();
		}

		protected override void OnMouseLeave(MouseEventArgs e)
		{
			base.OnMouseLeave(e);

			State.IsHovering = false;
			ResetStyle();
		}

		protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
		{
			base.OnMouseLeftButtonDown(e);

			// look for double clicks
			if (_doubleClickPossible)
				OnMouseDoubleClick(e);
			else
			{
				_doubleClickPossible = true;
				_doubleClickTimer.Start();
			}
			
			// see if this entity should be selected
			if (!e.Handled && (Keyboard.Modifiers == ModifierKeys.None || Keyboard.Modifiers == ModifierKeys.Shift))
			{
				if (Keyboard.Modifiers == ModifierKeys.None && !State.IsSelected) // no modifiers, not selected, deselect everything first
					DeselectAllAction();
				e.Handled = true;
				SelectAction();
				ResetStyle();
			}
		}

		protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
		{
			base.OnMouseLeftButtonUp(e);

			ParentCanvas.Actors.EndDrag(e);

			// stop everything from being deselected
			if (State.IsSelected)
				e.Handled = true;
		}

		/// <summary>
		/// Handler for the double click event.
		/// </summary>
		protected virtual void OnMouseDoubleClick(MouseButtonEventArgs e)
		{
		}

		private DispatcherTimer _doubleClickTimer;

		/// <summary>
		/// Gets set to true right after a button down event, then false after a short period of time.
		/// </summary>
		private bool _doubleClickPossible = false;

		/// <summary>
		/// Signals the end of the chance for a double click event.
		/// </summary>
		private void OnDoubleClickTick(object sender, EventArgs e)
		{
			_doubleClickPossible = false;
			_doubleClickTimer.Stop();
		}

		/// <summary>
		/// Returns true if the given point is within the actor.
		/// </summary>
		public virtual bool HitTest(Point pos)
		{
			return pos.X >= Position.X && pos.X <= Position.X + ActualWidth &&
				pos.Y >= Position.Y && pos.Y <= Position.Y + ActualHeight;
		}

		#endregion


		#region Dragging

		/// <summary>
		/// This gets called every time the actor is moved.
		/// </summary>
		public event ActorEventHandler Moved;

		/// <summary>
		/// Raise the moved event.
		/// </summary>
		protected void RaiseMoved()
		{
			if (Moved != null)
				Moved(this);
		}

		/// <summary>
        /// Tells the actor to begin a dragging operation.
        /// </summary>
		public virtual void OnBeginDrag(MouseButtonEventArgs e)
		{
			State.IsDragging = true;
			CaptureMouse();
		}

        /// <summary>
        /// Tells the actor to drag to the given point.
        /// </summary>
		public virtual void OnDrag(MouseEventArgs e)
		{
			if (State.IsDragging)
				RaiseMoved();
		}

        /// <summary>
        /// Tells the actor to end a dragging operation.
        /// </summary>
		public virtual void OnEndDrag(MouseButtonEventArgs e)
		{
			State.IsDragging = false;
			ReleaseMouseCapture();
		}

		/// <summary>
		/// Returns true if the mouse event is inside the actor.
		/// </summary>
		public bool HitTest(MouseEventArgs e)
		{
			var pos = e.GetPosition(this);
			return pos.X >= 0 && pos.X <= ActualWidth && pos.Y >= 0 && pos.Y <= ActualHeight;
		}

		#endregion


		#region Connections


		/// <summary>
		/// Get the point on the edge of the view that is intersected 
		/// by the line between the center and point.
		/// </summary>
		/// <param name="point">An external point.</param>
		/// <param name="dir">The direction of the connection (perpendicular to the edge).</param>
		/// <returns> The intersection point.</returns>
		public abstract Point GetConnectionPoint(Point point, out Point? dir);

		/// <summary>
		/// Computes the intersection of two line segments.
		/// </summary>
		/// <param name="beginLine1"> Beginning of the first line.</param>
		/// <param name="endLine1"> End of the first line.</param>
		/// <param name="beginLine2"> Beginning of the second line.</param>
		/// <param name="endLine2"> End of the second line.</param>
		/// <returns> The intersection point, or null if there is no intersection.</returns>
		/// <remarks> Uses formulas from http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/</remarks>
		public static Point? Intersect(Point beginLine1, Point endLine1, Point beginLine2, Point endLine2)
		{
			// compute the denominator
			double denom = (endLine2.Y - beginLine2.Y) * (endLine1.X - beginLine1.X) -
				(endLine1.Y - beginLine1.Y) * (endLine2.X - beginLine2.X);

			if (denom == 0) // lines are parallel (possible coincident, don't really care)
				return null;

			// compute the numerators
			double nume1 = ((endLine2.X - beginLine2.X) * (beginLine1.Y - beginLine2.Y)) -
					   ((endLine2.Y - beginLine2.Y) * (beginLine1.X - beginLine2.X));
			double nume2 = ((endLine1.X - beginLine1.X) * (beginLine1.Y - beginLine2.Y)) -
					   ((endLine1.Y - beginLine1.Y) * (beginLine1.X - beginLine2.X));
			double u1 = nume1 / denom;
			double u2 = nume2 / denom;

			// check for intersection
			if (u1 >= 0.0 && u1 <= 1.0 && u2 >= 0.0 && u2 <= 1.0)
			{
				Point intersect = new Point(beginLine1.X + u1 * (endLine1.X - beginLine1.X),
					beginLine1.Y + u1 * (endLine1.Y - beginLine1.Y));
				return intersect;
			}
			else // not intersecting
				return null;
		}

		#endregion


		#region Remote Mouse Interaction

		public void OnRemoteMouseEnter(MouseEventArgs e)
		{
			OnMouseEnter(e);
		}

		public void OnRemoteMouseLeave(MouseEventArgs e)
		{
			OnMouseLeave(e);
		}

		public void OnRemoteMouseLeftButtonDown(MouseButtonEventArgs e)
		{
			OnMouseLeftButtonDown(e);
		}

		public void OnRemoteMouseLeftButtonUp(MouseButtonEventArgs e)
		{
			OnMouseLeftButtonUp(e);
		}

		public void OnRemoteMouseMove(MouseEventArgs e)
		{
			OnMouseMove(e);
		}

		#endregion

	}
}
